using System;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Security.Permissions;

namespace LazyLoadDotNet.Domain
{
    public class LazyLoadProxy : RealProxy
    {
        private readonly IGhostable subject;

        [PermissionSet(SecurityAction.LinkDemand)]
        public LazyLoadProxy(int id, Type type) : base(type)
        {
            subject = (IGhostable) Activator.CreateInstance(type);
            InitializeAsGhost(id);
        }

        private void InitializeAsGhost(int id)
        {
            subject.Id = id;
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
        public override IMessage Invoke(IMessage msg)
        {
            MethodCallMessageWrapper wrappedMessage = new MethodCallMessageWrapper((IMethodCallMessage) msg);
            MethodInfo invokedMethodInfo = (MethodInfo) wrappedMessage.MethodBase;
            InvokeStrategy strategy = GetInvokeStrategy(invokedMethodInfo);
            return strategy.InvokeMethod(subject, invokedMethodInfo, wrappedMessage);
        }

        private InvokeStrategy GetInvokeStrategy(MethodInfo invokedMethodInfo)
        {
            if (!subject.IsGhost)
                return InvokeStrategy.NOT_GHOSTED;
            foreach ( PropertyInfo propertyInfo in GetProperties())
            {
                MethodInfo methodInfo = propertyInfo.GetGetMethod(false);
                if (IsSameMethod(invokedMethodInfo, methodInfo))
                    return InvokeStrategy.GETTER;
                methodInfo = propertyInfo.GetSetMethod(false);
                if (IsSameMethod(invokedMethodInfo, methodInfo))
                    return InvokeStrategy.SETTER;
            }
            return InvokeStrategy.METHOD;
        }

        private PropertyInfo[] GetProperties()
        {
            return subject.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
        }

        private static bool IsSameMethod(MethodInfo methodInfo, MethodInfo mi)
        {
            return mi != null && mi.Equals(methodInfo);
        }

        private abstract class InvokeStrategy
        {
            public static readonly InvokeStrategy METHOD = new InvokeMethodStrategy();
            public static readonly InvokeStrategy NOT_GHOSTED = new InvokeMethodStrategy();
            public static readonly InvokeStrategy GETTER = new InvokeGetterStrategy();
            public static readonly InvokeStrategy SETTER = new InvokeSetterStrategy();
            
            public abstract IMessage InvokeMethod(IGhostable subject, MethodInfo methodInfo, MethodCallMessageWrapper wrappedMessage);

            protected IMessage CreateReturnMessage(IGhostable subject, MethodInfo methodInfo,
                                                   MethodCallMessageWrapper wrappedMessage)
            {
                object outVal = methodInfo.Invoke(subject, wrappedMessage.Args);
                return new ReturnMessage(outVal, wrappedMessage.Args, wrappedMessage.Args.Length,
                                         wrappedMessage.LogicalCallContext, wrappedMessage);
            }                       
            
            private class InvokeGetterStrategy : InvokeStrategy
            {
                public override IMessage InvokeMethod(IGhostable subject, MethodInfo methodInfo,
                                                      MethodCallMessageWrapper wrappedMessage)
                {
                    if (subject.IsGhost)
                        subject.Load(subject);
                    return CreateReturnMessage(subject, methodInfo, wrappedMessage);
                }
            }
            
            private class InvokeSetterStrategy : InvokeStrategy
            {
                public override IMessage InvokeMethod(IGhostable subject, MethodInfo methodInfo,
                                                      MethodCallMessageWrapper wrappedMessage)
                {
                    subject.MarkLoaded();
                    return CreateReturnMessage(subject, methodInfo, wrappedMessage);
                }
            }

            private class InvokeMethodStrategy : InvokeStrategy
            {
                public override IMessage InvokeMethod(IGhostable subject, MethodInfo methodInfo, MethodCallMessageWrapper wrappedMessage)
                {
                    return CreateReturnMessage(subject, methodInfo, wrappedMessage);
                }
            }
        }
    }
}